/*
 * Copyright 2007-2008 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */

package jsr203.sun.nio.fs;

import java.io.IOException;
import java.security.AccessController;
import java.security.PrivilegedAction;
import jsr203.nio.file.AtomicMoveNotSupportedException;
import jsr203.nio.file.CopyOption;
import jsr203.nio.file.DirectoryNotEmptyException;
import jsr203.nio.file.FileAlreadyExistsException;
import jsr203.nio.file.LinkPermission;
import jsr203.nio.file.StandardCopyOption;
import static jsr203.sun.nio.fs.UnixConstants.EEXIST;
import static jsr203.sun.nio.fs.UnixConstants.EISDIR;
import static jsr203.sun.nio.fs.UnixConstants.ENOTEMPTY;
import static jsr203.sun.nio.fs.UnixConstants.EXDEV;
import static jsr203.sun.nio.fs.UnixConstants.O_CREAT;
import static jsr203.sun.nio.fs.UnixConstants.O_RDONLY;
import static jsr203.sun.nio.fs.UnixConstants.O_TRUNC;
import static jsr203.sun.nio.fs.UnixConstants.O_WRONLY;
import static jsr203.sun.nio.fs.UnixNativeDispatcher.chown;
import static jsr203.sun.nio.fs.UnixNativeDispatcher.close;
import static jsr203.sun.nio.fs.UnixNativeDispatcher.fchmod;
import static jsr203.sun.nio.fs.UnixNativeDispatcher.fchown;
import static jsr203.sun.nio.fs.UnixNativeDispatcher.futimes;
import static jsr203.sun.nio.fs.UnixNativeDispatcher.lchown;
import static jsr203.sun.nio.fs.UnixNativeDispatcher.mkdir;
import static jsr203.sun.nio.fs.UnixNativeDispatcher.mknod;
import static jsr203.sun.nio.fs.UnixNativeDispatcher.open;
import static jsr203.sun.nio.fs.UnixNativeDispatcher.readlink;
import static jsr203.sun.nio.fs.UnixNativeDispatcher.rename;
import static jsr203.sun.nio.fs.UnixNativeDispatcher.rmdir;
import static jsr203.sun.nio.fs.UnixNativeDispatcher.symlink;
import static jsr203.sun.nio.fs.UnixNativeDispatcher.unlink;
import static jsr203.sun.nio.fs.UnixNativeDispatcher.utimes;


/**
 * Unix implementation of Path#copyTo and Path#moveTo methods.
 */

class UnixCopyFile {
    private UnixCopyFile() {  }

    // copy directory from source to target
    private static void copyDirectory(UnixFileAttributes attrs,
                                      UnixPath target,
                                      boolean copyBasicAttributes,
                                      boolean failIfUnableToCopyBasic,
                                      boolean copyPosixAttributes,
                                      boolean failIfUnableToCopyPosix)
        throws IOException
    {
        byte[] path = target.getPathForSysCalls();
        try {
            mkdir(path, attrs.mode());
        } catch (UnixException x) {
            x.rethrowAsIOException(target);
        }
        boolean done = false;
        try {
            if (copyPosixAttributes){
                try {
                    chown(path, attrs.uid(), attrs.gid());
                } catch (UnixException x) {
                    // unable to set owner/group
                    if (failIfUnableToCopyPosix)
                        x.rethrowAsIOException(target);
                }
            }
            if (copyBasicAttributes) {
                try {
                    utimes(path, attrs.lastAccessTime(), attrs.lastModifiedTime());
                } catch (UnixException x) {
                    // unable to set times
                    if (failIfUnableToCopyBasic)
                        x.rethrowAsIOException(target);
                }
            }
            done = true;
        } finally {
            if (!done) {
                // rollback
                try { rmdir(path); } catch (UnixException ignore) { }
            }
        }
    }

    // copy regular file from source to target
    private static void copyFile(UnixPath source,
                                 UnixFileAttributes attrs,
                                 UnixPath  target,
                                 boolean copyBasicAttributes,
                                 boolean failIfUnableToCopyBasic,
                                 boolean copyPosixAttributes,
                                 boolean failIfUnableToCopyPosix,
                                 boolean copyExtendedAttributes,
                                 boolean failIfUnableToCopyEA)
        throws IOException
    {
        int fi = -1;
        try {
            fi = open(source.getPathForSysCalls(), O_RDONLY, 0);
        } catch (UnixException x) {
            x.rethrowAsIOException(source);
        }

        try {
            // open new file
            int fo = -1;
            try {
                fo = open(target.getPathForSysCalls(),
                           (O_WRONLY |
                            O_CREAT |
                            O_TRUNC),
                           attrs.mode());
            } catch (UnixException x) {
                x.rethrowAsIOException(target);
            }

            // set to true when file and attributes copied
            boolean complete = false;
            try {
                // transfer bytes to target file
                try {
                    transfer(fo, fi);
                } catch (UnixException x) {
                    x.rethrowAsIOException(source, target);
                }
                // copy owner/permissions
                if (copyPosixAttributes) {
                    try {
                        fchown(fo, attrs.uid(), attrs.gid());
                        fchmod(fo, attrs.mode());
                    } catch (UnixException x) {
                        if (failIfUnableToCopyPosix)
                            x.rethrowAsIOException(target);
                    }
                }
                // copy extended attributes (depends on file system)
                if (copyExtendedAttributes) {
                    source.getFileSystem().copyExtendedAttributes(fi, fo);
                }
                // copy time attributes
                if (copyBasicAttributes) {
                    try {
                        futimes(fo, attrs.lastAccessTime(), attrs.lastModifiedTime());
                    } catch (UnixException x) {
                        if (failIfUnableToCopyBasic)
                            x.rethrowAsIOException(target);
                    }
                }
                complete = true;
            } finally {
                close(fo);

                // copy of file or attributes failed so rollback
                if (!complete) {
                    try {
                        unlink(target.getPathForSysCalls());
                    } catch (UnixException ignore) { }
                }
            }
        } finally {
            close(fi);
        }
    }

    // copy symbolic link from source to target
    private static void copyLink(UnixPath source,
                                 UnixFileAttributes attrs,
                                 UnixPath  target,
                                 boolean copyOwner)
        throws IOException
    {
        byte[] linktarget = null;
        try {
            linktarget = readlink(source.getPathForSysCalls());
        } catch (UnixException x) {
            x.rethrowAsIOException(source);
        }
        byte[] path = target.getPathForSysCalls();
        try {
            symlink(linktarget, path);

            if (copyOwner) {
                try {
                    lchown(path, attrs.uid(), attrs.gid());
                } catch (UnixException x) {
                    // ignore since link attributes not required to be copied
                }
            }
        } catch (UnixException x) {
            x.rethrowAsIOException(target);
        }
    }

    // copy special file from source to target
    private static void copySpecial(UnixPath source,
                                    UnixFileAttributes attrs,
                                    UnixPath  target,
                                    boolean copyBasicAttributes,
                                    boolean failIfUnableToCopyBasic,
                                    boolean copyPosixAttributes,
                                    boolean failIfUnableToCopyPosix)
        throws IOException
    {
        byte[] path = target.getPathForSysCalls();
        try {
            mknod(path, attrs.mode(), attrs.rdev());
        } catch (UnixException x) {
            x.rethrowAsIOException(target);
        }
        boolean done = false;
        try {
            if (copyPosixAttributes) {
                try {
                    chown(path, attrs.uid(), attrs.gid());
                } catch (UnixException x) {
                    if (failIfUnableToCopyPosix)
                        x.rethrowAsIOException(target);
                }
            }
            if (copyBasicAttributes) {
                try {
                    utimes(path, attrs.lastAccessTime(), attrs.lastModifiedTime());
                } catch (UnixException x) {
                    if (failIfUnableToCopyBasic)
                        x.rethrowAsIOException(target);
                }
            }
            done = true;
        } finally {
            if (!done) {
                try { unlink(path); } catch (UnixException ignore) { }
            }
        }
    }

    // move file from source to target
    static void move(UnixPath source, UnixPath target, CopyOption... options)
        throws IOException
    {
        // permission check
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            source.checkWrite();
            target.checkWrite();
        }

        // map options
        boolean atomicMove = false;
        boolean replaceExisting = false;
        for (CopyOption option: options) {
            if (option == StandardCopyOption.ATOMIC_MOVE) {
                atomicMove = true;
                continue;
            }
            if (option == StandardCopyOption.REPLACE_EXISTING) {
                replaceExisting = true;
                continue;
            }
            if (option == null)
                throw new NullPointerException();
            throw new IllegalArgumentException("Unsupported or invalid copy option");
        }

        // handle atomic rename case
        if (atomicMove) {
            try {
                rename(source.getPathForSysCalls(), target.getPathForSysCalls());
            } catch (UnixException x) {
                if (x.errno() == EXDEV) {
                    throw new AtomicMoveNotSupportedException(
                        source.getPathForExecptionMessage(),
                        target.getPathForExecptionMessage(),
                        x.errorString());
                }
                x.rethrowAsIOException(source, target);
            }
            return;
        }

        // move using rename or copy+delete
        UnixFileAttributes sourceAttrs = null;
        UnixFileAttributes targetAttrs = null;

        // get attributes of source file (don't follow links)
        try {
            sourceAttrs = UnixFileAttributes.get(source.getPathForSysCalls(), false);
        } catch (UnixException x) {
            x.rethrowAsIOException(source);
        }

        // get attributes of target file (don't follow links)
        try {
            targetAttrs = UnixFileAttributes.get(target.getPathForSysCalls(), false);
        } catch (UnixException x) {
            // ignore
        }
        boolean targetExists = (targetAttrs != null);

        // if the target exists:
        // 1. check if source and target are the same file
        // 2. throw exception if REPLACE_EXISTING option is not set
        // 3. delete target if REPLACE_EXISTING option set
        if (targetExists) {
            if (sourceAttrs.isSameFile(targetAttrs))
                return;  // nothing to do as files are identical
            if (!replaceExisting) {
                throw new FileAlreadyExistsException(
                    target.getPathForExecptionMessage());
            }

            // attempt to delete target
            try {
                if (targetAttrs.isDirectory()) {
                    rmdir(target.getPathForSysCalls());
                } else {
                    unlink(target.getPathForSysCalls());
                }
            } catch (UnixException x) {
                // target is non-empty directory that can't be replaced.
                if (targetAttrs.isDirectory() &&
                   (x.errno() == EEXIST || x.errno() == ENOTEMPTY))
                {
                    throw new FileAlreadyExistsException(
                        source.getPathForExecptionMessage(),
                        target.getPathForExecptionMessage(),
                        x.getMessage());
                }
                x.rethrowAsIOException(target);
            }
        }

        // first try rename
        try {
            rename(source.getPathForSysCalls(), target.getPathForSysCalls());
            return;
        } catch (UnixException x) {
            if (x.errno() != EXDEV && x.errno() != EISDIR) {
                x.rethrowAsIOException(source, target);
            }
        }

        // copy source to target
        if (sourceAttrs.isDirectory()) {
            copyDirectory(sourceAttrs, target, true, true, true, false);
        } else {
            if (sourceAttrs.isSymbolicLink()) {
                copyLink(source, sourceAttrs, target, true);
            } else {
                if (sourceAttrs.isDevice()) {
                    copySpecial(source, sourceAttrs, target, true, true, true, false);
                } else {
                    copyFile(source, sourceAttrs, target, true, true,
                        true, false, true, false);
                }
            }
        }

        // delete source
        try {
            if (sourceAttrs.isDirectory()) {
                rmdir(source.getPathForSysCalls());
            } else {
                unlink(source.getPathForSysCalls());
            }
        } catch (UnixException x) {
            // file was copied but unable to unlink the source file so attempt
            // to remove the target and throw a reasonable exception
            try {
                if (sourceAttrs.isDirectory()) {
                    rmdir(target.getPathForSysCalls());
                } else {
                    unlink(target.getPathForSysCalls());
                }
            } catch (UnixException ignore) { }

            if (sourceAttrs.isDirectory() &&
                (x.errno() == EEXIST || x.errno() == ENOTEMPTY))
            {
                throw new DirectoryNotEmptyException(
                    source.getPathForExecptionMessage());
            }
            x.rethrowAsIOException(source);
        }
    }

    // copy file from source to target
    static void copy(UnixPath source, UnixPath target, CopyOption... options)
        throws IOException
    {
        // permission checks
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            source.checkRead();
            target.checkWrite();
        }

        // map options
        boolean replaceExisting = false;
        boolean copyBasicAttributes = false;
        boolean failIfUnableToCopyBasic = false;
        boolean copyPosixAttributes = false;
        boolean failIfUnableToCopyPosix = false;
        boolean copyNamedAttributes = false;
        boolean failIfUnableToCopyNamed = false;

        boolean followLinks = true;

        for (CopyOption option: options) {
            if (option == StandardCopyOption.REPLACE_EXISTING) {
                replaceExisting = true;
                continue;
            }
            if (option == StandardCopyOption.NOFOLLOW_LINKS) {
                followLinks = false;
                continue;
            }
            if (option == StandardCopyOption.COPY_ATTRIBUTES) {
                copyBasicAttributes = true;
                failIfUnableToCopyBasic = true;
                copyPosixAttributes = true;
                copyNamedAttributes = true;
                continue;
            }
            if (option == null)
                throw new NullPointerException();
            throw new IllegalArgumentException("Unsupported or invalid copy option");
        }

        UnixFileAttributes sourceAttrs = null;
        UnixFileAttributes targetAttrs = null;

        // get attributes of source file
        try {
            sourceAttrs = UnixFileAttributes
                .get(source.getPathForSysCalls(), followLinks);
        } catch (UnixException x) {
            x.rethrowAsIOException(source);
        }

        // if source file is symbolic link then we must check LinkPermission
        if (sm != null && sourceAttrs.isSymbolicLink()) {
            sm.checkPermission(new LinkPermission("symbolic"));
        }

        // get attributes of target file (don't follow links)
        try {
            targetAttrs = UnixFileAttributes
                .get(target.getPathForSysCalls(), false);
        } catch (UnixException x) {
            // ignore
        }
        boolean targetExists = (targetAttrs != null);

        // if the target exists:
        // 1. check if source and target are the same file
        // 2. throw exception if REPLACE_EXISTING option is not set
        // 3. try to unlink the target
        if (targetExists) {
            if (sourceAttrs.isSameFile(targetAttrs))
                return;  // nothing to do as files are identical
            if (!replaceExisting)
                throw new FileAlreadyExistsException(
                    target.getPathForExecptionMessage());
            try {
                if (targetAttrs.isDirectory()) {
                    rmdir(target.getPathForSysCalls());
                } else {
                    unlink(target.getPathForSysCalls());
                }
            } catch (UnixException x) {
                // target is non-empty directory that can't be replaced.
                if (targetAttrs.isDirectory() &&
                   (x.errno() == EEXIST || x.errno() == ENOTEMPTY))
                {
                    throw new FileAlreadyExistsException(
                        source.getPathForExecptionMessage(),
                        target.getPathForExecptionMessage(),
                        x.getMessage());
                }
                x.rethrowAsIOException(target);
            }
        }

        // do the copy
        if (sourceAttrs.isDirectory()) {
            copyDirectory(sourceAttrs, target, copyBasicAttributes, failIfUnableToCopyBasic,
                copyPosixAttributes, failIfUnableToCopyPosix);
        } else {
            if (sourceAttrs.isSymbolicLink()) {
                copyLink(source, sourceAttrs, target, copyPosixAttributes);
            } else {
                copyFile(source, sourceAttrs, target,
                    copyBasicAttributes, failIfUnableToCopyBasic,
                    copyPosixAttributes, failIfUnableToCopyPosix,
                    copyNamedAttributes, failIfUnableToCopyNamed);
            }
        }
    }

    // -- native methods --

    static native void transfer(int dst, int src) throws UnixException;

    static {
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            @Override
            public Void run() {
                System.loadLibrary("nio");
                return null;
            }});
    }

}
